Skip to main content
Glama
+page.svelte18.2 kB
<script> import { page } from '$app/stores'; import { onMount } from 'svelte'; let task = null; let linkedPrd = null; // 연결된 PRD 정보 let linkedDesign = null; // 연결된 설계 정보 let linkedDocuments = []; // 연결된 문서들 let additionalConnections = []; // 추가 연결들 let loading = true; let error = null; function formatDate(dateValue) { if (!dateValue) return '-'; try { let date; if (typeof dateValue === 'string' && dateValue.includes('T')) { date = new Date(dateValue); } else if (typeof dateValue === 'string' && /^\d+\.?\d*$/.test(dateValue)) { date = new Date(parseFloat(dateValue)); } else if (typeof dateValue === 'number') { date = new Date(dateValue); } else { date = new Date(dateValue); } if (isNaN(date.getTime())) { return '-'; } return date.toLocaleString('ko-KR', { year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false }); } catch (error) { console.error('Date formatting error:', error, dateValue); return '-'; } } // 연결 상세 정보를 로드하는 함수 async function loadConnectionDetails(connections) { const detailedConnections = []; for (const conn of connections) { try { let detailResponse; let entityData = null; switch (conn.entity_type) { case 'prd': detailResponse = await fetch(`/api/prds/${conn.entity_id}`); break; case 'design': detailResponse = await fetch(`/api/designs/${conn.entity_id}`); break; case 'test': detailResponse = await fetch(`/api/tests/${conn.entity_id}`); break; case 'document': detailResponse = await fetch(`/api/documents/${conn.entity_id}`); break; default: continue; } if (detailResponse && detailResponse.ok) { entityData = await detailResponse.json(); } detailedConnections.push({ ...conn, entityData, title: entityData?.title || entityData?.name || `ID: ${conn.entity_id}`, type_label: getConnectionTypeLabel(conn.entity_type) }); } catch (e) { console.error(`연결 상세 정보 로드 실패 (${conn.entity_type}:${conn.entity_id}):`, e); } } return detailedConnections; } // 연결 유형별 표시 이름을 반환하는 함수 function getConnectionTypeLabel(type) { const labels = { prd: '요구사항', design: '설계', test: '테스트', document: '문서', other: '기타' }; return labels[type] || type; } onMount(async () => { try { const response = await fetch(`/api/tasks/${$page.params.id}`); if (response.ok) { task = await response.json(); // MCP로 저장된 JSON 필드들 파싱 if (task.acceptanceCriteria && typeof task.acceptanceCriteria === 'string') { try { // 이중 JSON 인코딩된 경우 처리 let parsed = JSON.parse(task.acceptanceCriteria); if (typeof parsed === 'string') { parsed = JSON.parse(parsed); } task.acceptanceCriteria = Array.isArray(parsed) ? parsed : []; } catch (e) { console.warn('acceptance_criteria 파싱 실패:', e); task.acceptanceCriteria = []; } } // 연결된 PRD가 있으면 로드 if (task.prd_id) { const prdResponse = await fetch(`/api/prds/${task.prd_id}`); if (prdResponse.ok) { linkedPrd = await prdResponse.json(); } } // 연결된 설계가 있으면 로드 if (task.design_id) { const designResponse = await fetch(`/api/designs/${task.design_id}`); if (designResponse.ok) { linkedDesign = await designResponse.json(); } } // 연결된 문서들 로드 try { const documentsResponse = await fetch(`/api/documents?linked_entity_type=task&linked_entity_id=${task.id}`); if (documentsResponse.ok) { const documentsData = await documentsResponse.json(); linkedDocuments = documentsData.documents || []; } } catch (e) { console.error('연결된 문서 로딩 실패:', e); } // 추가 연결들 로드 try { const connectionsResponse = await fetch(`/api/tasks/${task.id}/connections`); if (connectionsResponse.ok) { const connections = await connectionsResponse.json(); additionalConnections = await loadConnectionDetails(connections); } } catch (e) { console.error('추가 연결 로딩 실패:', e); } } else { error = '작업을 찾을 수 없습니다'; } } catch (e) { error = '데이터를 불러오는 중 오류가 발생했습니다'; } finally { loading = false; } }); </script> <svelte:head> <title>{task?.title || '작업 상세보기'} - WorkflowMCP</title> </svelte:head> <div class="max-w-4xl mx-auto space-y-6"> <div class="flex items-center justify-between"> <div> <h1 class="text-3xl font-bold text-gray-900">작업 상세보기</h1> <p class="text-gray-600 mt-1">작업 상세 정보</p> </div> <div class="flex space-x-3"> <a href="/tasks" class="btn btn-secondary">← 목록으로</a> {#if task} <a href="/tasks/{task.id}/edit" class="btn btn-primary">편집</a> {/if} </div> </div> {#if loading} <div class="flex justify-center py-12"> <div class="text-gray-500">데이터를 불러오는 중...</div> </div> {:else if error} <div class="bg-red-50 border border-red-200 rounded-md p-4"> <div class="text-red-800">{error}</div> </div> {:else if task} <div class="space-y-6"> <!-- 기본 정보 --> <div class="card"> <h2 class="text-xl font-semibold text-gray-900 mb-4">기본 정보</h2> <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div> <label class="block text-sm font-medium text-gray-700 mb-1">제목</label> <div class="text-gray-900 font-medium">{task.title}</div> <div class="text-xs text-gray-500 mt-1 font-mono">ID: {task.id}</div> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-1">상태</label> <span class="badge {task.status === 'done' ? 'bg-green-100 text-green-800' : task.status === 'in_progress' ? 'bg-blue-100 text-blue-800' : task.status === 'blocked' ? 'bg-red-100 text-red-800' : 'bg-gray-100 text-gray-800'}"> {task.status === 'done' ? '완료' : task.status === 'in_progress' ? '진행중' : task.status === 'blocked' ? '차단됨' : '대기중'} </span> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-1">우선순위</label> <span class="badge {task.priority === 'high' ? 'bg-red-100 text-red-800' : task.priority === 'medium' ? 'bg-yellow-100 text-yellow-800' : 'bg-green-100 text-green-800'}"> {task.priority === 'high' ? '높음' : task.priority === 'medium' ? '보통' : '낮음'} </span> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-1">담당자</label> <div class="text-gray-600">{task.assignee || '-'}</div> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-1">예상 시간</label> <div class="text-gray-600">{task.estimatedHours || 0}시간</div> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-1">실제 시간</label> <div class="text-gray-600">{task.actualHours || 0}시간</div> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-1">마감일</label> <div class="text-gray-600">{task.dueDate ? formatDate(task.dueDate) : '-'}</div> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-1">생성일</label> <div class="text-gray-600">{formatDate(task.createdAt)}</div> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-1">최종 수정일</label> <div class="text-gray-600">{formatDate(task.updatedAt)}</div> </div> </div> {#if task.description} <div class="mt-4"> <label class="block text-sm font-medium text-gray-700 mb-1">설명</label> <div class="text-gray-900 whitespace-pre-wrap">{task.description}</div> </div> {/if} </div> <!-- 연결된 요구사항 --> {#if linkedPrd} <div class="card"> <h2 class="text-xl font-semibold text-gray-900 mb-4">연결된 요구사항</h2> <div class="p-4 bg-gray-50 rounded-lg border"> <div class="flex items-start justify-between mb-2"> <div> <h3 class="font-medium text-gray-900 mb-1"> <a href="/prds/{linkedPrd.id}" class="text-blue-600 hover:text-blue-800 hover:underline"> {linkedPrd.title} </a> </h3> <div class="flex items-center space-x-2 mb-2"> <span class="badge {linkedPrd.priority === 'high' ? 'bg-red-100 text-red-800' : linkedPrd.priority === 'medium' ? 'bg-yellow-100 text-yellow-800' : 'bg-gray-100 text-gray-800'}"> {linkedPrd.priority === 'high' ? '높음' : linkedPrd.priority === 'medium' ? '보통' : '낮음'} </span> <span class="badge {linkedPrd.status === 'active' ? 'bg-green-100 text-green-800' : linkedPrd.status === 'completed' ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-800'}"> {linkedPrd.status === 'active' ? '활성' : linkedPrd.status === 'completed' ? '완료' : linkedPrd.status === 'draft' ? '초안' : linkedPrd.status} </span> </div> </div> </div> {#if linkedPrd.description} <div class="text-gray-600 text-sm"> {linkedPrd.description.length > 150 ? linkedPrd.description.substring(0, 150) + '...' : linkedPrd.description} </div> {/if} </div> </div> {/if} <!-- 연결된 설계 --> {#if linkedDesign} <div class="card"> <h2 class="text-xl font-semibold text-gray-900 mb-4">연결된 설계</h2> <div class="p-4 bg-gray-50 rounded-lg border"> <div class="flex items-start justify-between mb-2"> <div> <h3 class="font-medium text-gray-900 mb-1"> <a href="/designs/{linkedDesign.id}" class="text-blue-600 hover:text-blue-800 hover:underline"> {linkedDesign.title} </a> </h3> <div class="flex items-center space-x-2 mb-2"> <span class="badge {linkedDesign.design_type === 'system' ? 'bg-blue-100 text-blue-800' : linkedDesign.design_type === 'architecture' ? 'bg-purple-100 text-purple-800' : linkedDesign.design_type === 'ui_ux' ? 'bg-pink-100 text-pink-800' : linkedDesign.design_type === 'database' ? 'bg-green-100 text-green-800' : 'bg-orange-100 text-orange-800'}"> {linkedDesign.design_type === 'system' ? '시스템' : linkedDesign.design_type === 'architecture' ? '아키텍처' : linkedDesign.design_type === 'ui_ux' ? 'UI/UX' : linkedDesign.design_type === 'database' ? '데이터베이스' : 'API'} </span> <span class="badge {linkedDesign.priority === 'high' ? 'bg-red-100 text-red-800' : linkedDesign.priority === 'medium' ? 'bg-yellow-100 text-yellow-800' : 'bg-gray-100 text-gray-800'}"> {linkedDesign.priority === 'high' ? '높음' : linkedDesign.priority === 'medium' ? '보통' : '낮음'} </span> <span class="badge {linkedDesign.status === 'draft' ? 'bg-gray-100 text-gray-800' : linkedDesign.status === 'review' ? 'bg-yellow-100 text-yellow-800' : linkedDesign.status === 'approved' ? 'bg-green-100 text-green-800' : 'bg-blue-100 text-blue-800'}"> {linkedDesign.status === 'draft' ? '초안' : linkedDesign.status === 'review' ? '검토중' : linkedDesign.status === 'approved' ? '승인됨' : '구현완료'} </span> </div> </div> </div> {#if linkedDesign.description} <div class="text-gray-600 text-sm"> {linkedDesign.description.length > 150 ? linkedDesign.description.substring(0, 150) + '...' : linkedDesign.description} </div> {/if} </div> </div> {/if} <!-- 연결된 문서 --> {#if linkedDocuments && linkedDocuments.length > 0} <div class="card"> <h2 class="text-xl font-semibold text-gray-900 mb-4">연결된 문서</h2> <div class="space-y-3"> {#each linkedDocuments as document} <div class="p-4 bg-gray-50 rounded-lg border hover:bg-gray-100 transition-colors"> <div class="flex items-start justify-between mb-2"> <div class="flex-1"> <h3 class="font-medium text-gray-900 mb-1"> <a href="/documents/{document.id}" class="text-blue-600 hover:text-blue-800 hover:underline"> {document.title} </a> </h3> <div class="flex items-center space-x-2 mb-2"> <span class="badge bg-blue-100 text-blue-800"> {document.doc_type === 'test_guide' ? '테스트 가이드' : document.doc_type === 'test_results' ? '테스트 결과' : document.doc_type === 'analysis' ? '분석' : document.doc_type === 'report' ? '보고서' : document.doc_type === 'checklist' ? '체크리스트' : document.doc_type === 'specification' ? '사양서' : document.doc_type === 'meeting_notes' ? '회의록' : document.doc_type === 'decision_log' ? '의사결정 로그' : document.doc_type} </span> {#if document.link_type} <span class="badge bg-green-100 text-green-800"> {document.link_type} </span> {/if} {#if document.category} <span class="badge bg-gray-100 text-gray-800"> {document.category} </span> {/if} </div> </div> <div class="text-xs text-gray-500"> ID: {document.id} </div> </div> {#if document.summary} <div class="text-gray-600 text-sm"> {document.summary.length > 150 ? document.summary.substring(0, 150) + '...' : document.summary} </div> {/if} {#if document.tags && document.tags.length > 0} <div class="mt-2 flex flex-wrap gap-1"> {#each document.tags as tag} <span class="inline-flex items-center px-2 py-0.5 rounded text-xs bg-blue-50 text-blue-700"> {tag} </span> {/each} </div> {/if} </div> {/each} </div> </div> {/if} <!-- 추가 연결 --> {#if additionalConnections && additionalConnections.length > 0} <div class="card"> <h2 class="text-xl font-semibold text-gray-900 mb-4">추가 연결</h2> <div class="space-y-3"> {#each additionalConnections as connection} <div class="p-4 bg-gray-50 rounded-lg border hover:bg-gray-100 transition-colors"> <div class="flex items-start justify-between mb-2"> <div class="flex-1"> <h3 class="font-medium text-gray-900 mb-1"> <a href="/{connection.entity_type}s/{connection.entity_id}" class="text-blue-600 hover:text-blue-800 hover:underline"> {connection.title} </a> </h3> <div class="flex items-center space-x-2 mb-2"> <span class="badge bg-purple-100 text-purple-800"> {connection.type_label} </span> {#if connection.entityData?.status} <span class="badge bg-green-100 text-green-800"> {connection.entityData.status} </span> {/if} </div> </div> <div class="text-xs text-gray-500"> ID: {connection.entity_id} </div> </div> {#if connection.entityData?.description || connection.entityData?.summary} <div class="text-gray-600 text-sm"> {(connection.entityData.description || connection.entityData.summary || '').length > 150 ? (connection.entityData.description || connection.entityData.summary).substring(0, 150) + '...' : (connection.entityData.description || connection.entityData.summary)} </div> {/if} </div> {/each} </div> </div> {/if} <!-- 태그 --> {#if task.tags && task.tags.length > 0} <div class="card"> <h2 class="text-xl font-semibold text-gray-900 mb-4">태그</h2> <div class="flex flex-wrap gap-2"> {#each task.tags as tag} <span class="badge bg-blue-100 text-blue-800">{tag}</span> {/each} </div> </div> {/if} <!-- 상세 내용 --> {#if task.details} <div class="card"> <h2 class="text-xl font-semibold text-gray-900 mb-4">상세 내용</h2> <div class="text-gray-900 whitespace-pre-wrap">{task.details}</div> </div> {/if} <!-- 완료 기준 --> {#if task.acceptanceCriteria && task.acceptanceCriteria.length > 0} <div class="card"> <h2 class="text-xl font-semibold text-gray-900 mb-4">완료 기준</h2> <div class="space-y-3"> {#each task.acceptanceCriteria as criteria, index} <div class="flex items-start space-x-3 p-3 bg-gray-50 rounded-lg"> <span class="text-sm text-gray-500 font-medium min-w-0">{index + 1}.</span> <span class="text-sm text-gray-700">{criteria}</span> </div> {/each} </div> </div> {/if} <!-- 테스트 전략 --> {#if task.testStrategy} <div class="card"> <h2 class="text-xl font-semibold text-gray-900 mb-4">테스트 전략</h2> <div class="text-gray-900 whitespace-pre-wrap">{task.testStrategy}</div> </div> {/if} <!-- 메모 --> {#if task.notes} <div class="card"> <h2 class="text-xl font-semibold text-gray-900 mb-4">메모</h2> <div class="text-gray-900 whitespace-pre-wrap">{task.notes}</div> </div> {/if} </div> {/if} </div> <style> .badge { @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium; } .card { @apply bg-white rounded-lg shadow p-6; } .btn { @apply px-4 py-2 rounded-md font-medium transition-colors; } .btn-primary { @apply bg-blue-600 text-white hover:bg-blue-700; } .btn-secondary { @apply bg-gray-200 text-gray-900 hover:bg-gray-300; } </style>

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/foswmine/workflow-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server